/*
 * Open Source Physics software is free software as described near the bottom of this code file.
 *
 * For additional information and documentation on Open Source Physics please see:
 * <http://www.opensourcephysics.org/>
 */

package org.opensourcephysics.display3d.simple3d;
import org.opensourcephysics.display.MouseController;
import java.util.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.print.*;
import org.opensourcephysics.controls.*;
import org.opensourcephysics.display3d.core.interaction.*;
import java.awt.image.BufferedImage;
import javax.swing.SwingUtilities;
import java.lang.reflect.*;
import org.opensourcephysics.display.TextPanel;
import org.opensourcephysics.display.OSPLayout;
import javax.swing.JViewport;
import org.opensourcephysics.tools.VideoTool;

/**
 *
 * <p>Title: DrawingPanel3D</p>
 *
 * <p>Description: The simple3D implementation of a DrawingPanel3D.</p>
 *
 * <p>Interaction: The panel has only one target, the panel itself.
 * If enabled, the panel issues MOUSE_ENTER, MOUSE_EXIT,
 * MOUSE_MOVED, and MOUSE_DRAGGED InteractionEvents with target=null.
 * When the ALT key is held, the panel also issues MOUSE_PRESSED,
 * MOUSE_DRAGGED (again), and MOUSE_RELEASED InteractionEvents.
 * In this second case, the getInfo() method of the event returns a double[3]
 * with the coordinates of the point selected.</p>
 * <p>Even if the panel is disabled, the panel can be panned, zoomed and (in 3D
 * modes) rotated and the elements in it can be enabled.</p>
 * <p>The interaction capabilities are not XML serialized.</p>
 *
 * <p>Copyright: Open Source Physics project</p>
 * @author Francisco Esquembre
 * @version June 2005
 */
public class DrawingPanel3D extends javax.swing.JPanel implements org.opensourcephysics.display.Renderable, org.opensourcephysics.display3d.core.DrawingPanel3D, Printable, ActionListener {
   static private final int AXIS_DIVISIONS = 10;
   static private final Color bgColor = new Color(239, 239, 255);
   // Configuration variables
   private double xmin, xmax, ymin, ymax, zmin, zmax;
   private VisualizationHints visHints = null;
   private Camera camera = null;
   // Implementation variables
   private boolean quickRedrawOn = false, squareAspect = true;
   private double centerX, centerY, centerZ, maximumSize;
   private double aconstant, bconstant;
   private int acenter, bcenter;
   private ArrayList list3D = new ArrayList();
   private ArrayList decorationList = new ArrayList();
   private ArrayList elementList = new ArrayList();
   private Object3D.Comparator3D comparator = new Object3D.Comparator3D();   // see class Comparator3D below
   // Variables for decoration
   private ElementArrow xAxis, yAxis, zAxis;
   private ElementText xText, yText, zText;
   private ElementSegment[] boxSides = new ElementSegment[12];
   // Variables for interaction
   private final InteractionTarget myTarget = new InteractionTarget(null, 0);
   private int trackersVisible, keyPressed = -1;
   private int lastX = 0, lastY = 0;
   private InteractionTarget targetHit = null, targetEntered = null;
   private double[] trackerPoint = null;
   private ArrayList listeners = new ArrayList();
   private ElementSegment[] trackerLines = null;
   // Variables for painting
   volatile private boolean dirtyImage = true;                               // offscreenImage needs to be recomputed
   // the image that will be copied to the screen
   volatile private BufferedImage offscreenImage = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);
   // the image into which we will draw
   private BufferedImage workingImage = offscreenImage;
   private javax.swing.Timer updateTimer = new javax.swing.Timer(100, this); // delay before updating the panel
   private boolean needResize = true, needsToRecompute = true;
   // Variables for Messages
   protected TextPanel trMessageBox = new TextPanel();                       // text box in top right hand corner for message
   protected TextPanel tlMessageBox = new TextPanel();                       // text box in top left hand corner for message
   protected TextPanel brMessageBox = new TextPanel();                       // text box in lower right hand corner for message
   protected TextPanel blMessageBox = new TextPanel();                       // text box in lower left hand corner for mouse coordinates
   protected GlassPanel glassPanel = new GlassPanel();
   protected OSPLayout glassPanelLayout = new OSPLayout();
   protected Rectangle viewRect = null;                                      // the clipping rectangle within a scroll pane viewport
   /**
    * The video capture tool for this panel.
    */
   protected VideoTool vidCap;

   public DrawingPanel3D() {
      // GlassPanel for messages
      glassPanel.setLayout(glassPanelLayout);
      super.setLayout(new BorderLayout());
      glassPanel.add(trMessageBox, OSPLayout.TOP_RIGHT_CORNER);
      glassPanel.add(tlMessageBox, OSPLayout.TOP_LEFT_CORNER);
      glassPanel.add(brMessageBox, OSPLayout.BOTTOM_RIGHT_CORNER);
      glassPanel.add(blMessageBox, OSPLayout.BOTTOM_LEFT_CORNER);
      glassPanel.setOpaque(false);
      super.add(glassPanel, BorderLayout.CENTER);
      setBackground(bgColor);
      setPreferredSize(new Dimension(300, 300));
      visHints = new VisualizationHints(this);
      camera = new Camera(this);
      addComponentListener(new java.awt.event.ComponentAdapter() {

         public void componentResized(java.awt.event.ComponentEvent e) {
            needResize = true;
            dirtyImage = true;
         }
      });
      IADMouseController mouseController = new IADMouseController();
      addMouseListener(mouseController);
      addMouseMotionListener(mouseController);
      addKeyListener(new java.awt.event.KeyAdapter() {

         public void keyPressed(java.awt.event.KeyEvent _e) {
            keyPressed = _e.getKeyCode();
//            System.out.println("Key = "+keyPressed);
         }

         public void keyReleased(java.awt.event.KeyEvent _e) {
            keyPressed = -1;
         }
      });
      this.setFocusable(true);
      /* Decoration of the scene */
      // Create the bounding box
      Resolution axesRes = new Resolution(AXIS_DIVISIONS, 1, 1);
      for(int i = 0, n = boxSides.length; i<n; i++) {
         boxSides[i] = new ElementSegment();
         boxSides[i].getRealStyle().setResolution(axesRes);
         boxSides[i].setPanel(this); // Because I don't add it to the panel in the standard way
         decorationList.add(boxSides[i]);
      }
      boxSides[0].getStyle().setLineColor(new Color(128, 0, 0));
      boxSides[3].getStyle().setLineColor(new Color(0, 128, 0));
      boxSides[8].getStyle().setLineColor(new Color(0, 0, 255));
      // Create the axes
      String[] axesLabels = visHints.getAxesLabels();
      xAxis = new ElementArrow();
      xAxis.getRealStyle().setResolution(axesRes);
      xAxis.getStyle().setFillColor(new Color(128, 0, 0));
      xAxis.setPanel(this);
      decorationList.add(xAxis);
      xText = new ElementText();
      xText.setText(axesLabels[0]);
      xText.setJustification(ElementText.JUSTIFICATION_CENTER);
      xText.getRealStyle().setLineColor(Color.BLACK);
      xText.setFont(new Font("Dialog", Font.PLAIN, 12));
      xText.setPanel(this);
      decorationList.add(xText);
      yAxis = new ElementArrow();
      yAxis.getRealStyle().setResolution(axesRes);
      yAxis.getStyle().setFillColor(new Color(0, 128, 0));
      yAxis.setPanel(this);
      decorationList.add(yAxis);
      yText = new ElementText();
      yText.setText(axesLabels[1]);
      yText.setJustification(ElementText.JUSTIFICATION_CENTER);
      yText.getRealStyle().setLineColor(Color.BLACK);
      yText.setFont(new Font("Dialog", Font.PLAIN, 12));
      yText.setPanel(this);
      decorationList.add(yText);
      zAxis = new ElementArrow();
      zAxis.getRealStyle().setResolution(axesRes);
      zAxis.getStyle().setFillColor(new Color(0, 0, 255));
      zAxis.setPanel(this);
      decorationList.add(zAxis);
      zText = new ElementText();
      zText.setText(axesLabels[2]);
      zText.setJustification(ElementText.JUSTIFICATION_CENTER);
      zText.getRealStyle().setLineColor(Color.BLACK);
      zText.setFont(new Font("Dialog", Font.PLAIN, 12));
      zText.setPanel(this);
      decorationList.add(zText);
      // Create the trackers
      trackerLines = new ElementSegment[9];
      for(int i = 0, n = trackerLines.length; i<n; i++) {
         trackerLines[i] = new ElementSegment();
         trackerLines[i].getRealStyle().setResolution(axesRes);
         trackerLines[i].setVisible(false);
         trackerLines[i].setPanel(this);
         decorationList.add(trackerLines[i]);
      }
      setCursorMode(); // compute the correct value for trackersVisible
      /* End of decoration */
      // Set default for displayMode
      if(camera.is3dMode()) {
         visHints.setDecorationType(VisualizationHints.DECORATION_CUBE);
         visHints.setUseColorDepth(true);
      } else {
         visHints.setDecorationType(VisualizationHints.DECORATION_NONE);
         visHints.setUseColorDepth(false);
      }
      setPreferredMinMax(-1, 1, -1, 1, -1, 1);
   }

   // ---------------------------------
   // Begin W. Christian additions and changes
   // ---------------------------------

   /**
    * Performs an action for the update timer by rendering a new background image
    * @param  evt
    */
   public void actionPerformed(ActionEvent evt) { // render a new image if the current image is dirty
      if(dirtyImage||needsUpdate()) {
         render(); // renders the scene from within the timer thread
      }
   }

   public void setIgnoreRepaint(boolean ignoreRepaint) {
      super.setIgnoreRepaint(ignoreRepaint);
      glassPanel.setIgnoreRepaint(ignoreRepaint);
   }

   /**
    * Update the panel's buffered image from within a separate timer thread.
    */
   private void updatePanel() {
      if(getIgnoreRepaint()) {
         return; // the animation thread will take care of the update
      }
      updateTimer.setRepeats(false); // perform only one render event
      updateTimer.setCoalesce(true); // coalesce render events
      updateTimer.start();           // start update timer
   }

   /**
    * Paints the component by copying the offscreen image into the graphics context.
    * @param g Graphics
    */
   public void paintComponent(Graphics g) {
      // find the clipping rectangle within a scroll pane viewport
      viewRect = null;
      Container c = getParent();
      while(c!=null) {
         if(c instanceof JViewport) {
            viewRect = ((JViewport) c).getViewRect();
            glassPanel.setBounds(viewRect);
            glassPanelLayout.checkLayoutRect(glassPanel, viewRect);
            break;
         }
         c = c.getParent();
      }
      int xoff = (getWidth()-offscreenImage.getWidth())/2;
      int yoff = (getHeight()-offscreenImage.getHeight())/2;
      g.drawImage(offscreenImage, xoff, yoff, null); // copy image to the center of the panel
      if(dirtyImage||needsUpdate()) { // Paco : can this be commented out?
         updatePanel();               // starts an update timer event
      }
   }

   /**
    * Invalidates this component.  This component and all parents
    * above it are marked as needing to be laid out.  This method can
    * be called often, so it needs to execute quickly.
    * @see       #validate
    * @see       #doLayout
    * @see       LayoutManager
    * @since     JDK1.0
    */
   public void invalidate(){
      needResize = true;
      super.invalidate();
   }


   public BufferedImage render(BufferedImage image) {
      Graphics g = image.getGraphics();
      paintEverything(g, image.getWidth(null), image.getHeight(null));
      Rectangle viewRect = this.viewRect; // reference for thread safety
      if(viewRect!=null) {
         Rectangle r = new Rectangle(0, 0, image.getWidth(null), image.getHeight(null));
         glassPanel.setBounds(r);
         glassPanelLayout.checkLayoutRect(glassPanel, r);
         glassPanel.render(g);
         glassPanel.setBounds(viewRect);
         glassPanelLayout.checkLayoutRect(glassPanel, viewRect);
      } else {
         glassPanel.render(g);
      }
      g.dispose(); // Disposes of the graphics context and releases any system resources that it is using.
      return image;
   }

   public BufferedImage render() {
      if(!isShowing()||isIconified()) { // don't render if panel cannot be seen
         needsToRecompute = true;       // make sure we recompute later when we are showing
         return null;                   // no need to render if the frame is not visible
      }
      BufferedImage workingImage = checkImageSize(this.workingImage);
      synchronized(workingImage) {           // do not let threads access workingImage while it is being painted
         if(needResize) {
            computeConstants(workingImage.getWidth(), workingImage.getHeight());
            needResize = false;
         }
         render(workingImage);
         // swap the images
         this.workingImage = offscreenImage; // use current offscreen image for the next drawing
         offscreenImage = workingImage;      // recently drawn image is now the offscreenImage
         dirtyImage = false;                 // offscreenImage is up to date
      }
      // the offscreenImage is now ready to be copied to the screen
      // always update a Swing component from the event thread
      if(SwingUtilities.isEventDispatchThread()) {
         paintImmediately(getVisibleRect()); // we are already within the event thread so DO IT!
      } else {                               // paint within the event thread
         Runnable doNow = new Runnable() {   // runnable object will be called by invokeAndWait

            public void run() {
               paintImmediately(getVisibleRect());
            }
         };
         try {
            SwingUtilities.invokeAndWait(doNow);
         } // wait for the paint operation to finish; should be fast
             catch(InvocationTargetException ex) {}
         catch(InterruptedException ex) {}
      }
      if(vidCap!=null && offscreenImage!=null && vidCap.isRecording()) { // buffered image should exists so use it.
            vidCap.addFrame(offscreenImage);
      }
      return workingImage;
   }

   /**
    * Whether the image is dirty or any of the elements has changed
    * @return boolean
    */
   private final boolean needsUpdate() {
      for(Iterator it = elementList.iterator(); it.hasNext(); ) {
         if(((Element) it.next()).getElementChanged()) {
            return true;
         }
      }
      return false;
   }

   /**
    * Checks the image to see if the working image has the correct Dimension.
    * @return <code>true <\code> if the offscreen image matches the panel;  <code>false <\code> otherwise
    */
   private BufferedImage checkImageSize(BufferedImage image) {
      int width = getWidth(), height = getHeight();
      if((width<=2)||(height<=2)) { // image is too small to draw anything useful
         return new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);
      }
      if((image==null)||(width!=image.getWidth())||(height!=image.getHeight())) {
         // a new image with the correct size will be created
         return getGraphicsConfiguration().createCompatibleImage(width, height);
      }
      return image; // given image is the correct size
   }

   /**
    * Gets the iconified flag from the top level frame.
    * @return boolean true if frame is iconified; false otherwise
    */
   private boolean isIconified() {
      Component c = getTopLevelAncestor();
      if(c instanceof Frame) {
         return(((Frame) c).getExtendedState()&Frame.ICONIFIED)==1;
      } else {
         return false;
      }
   }

   // ---------------------------------
   // end of W. Christian additions and changes
   // ---------------------------------
   // ---------------------------------
   // Implementation of core.DrawingPanel3D
   // ---------------------------------
   public java.awt.Component getComponent() {
      return this;
   }

   public void setPreferredMinMax(double minX, double maxX, double minY, double maxY, double minZ, double maxZ) {
      this.xmin = minX;
      this.xmax = maxX;
      this.ymin = minY;
      this.ymax = maxY;
      this.zmin = minZ;
      this.zmax = maxZ;
      centerX = (xmax+xmin)/2.0;
      centerY = (ymax+ymin)/2.0;
      centerZ = (zmax+zmin)/2.0;
      maximumSize = getMaximum3DSize();
      resetDecoration(xmax-xmin, ymax-ymin, zmax-zmin);
      camera.reset();
      needsToRecompute = true;
      dirtyImage = true;
   }

   final public double getPreferredMinX() {
      return this.xmin;
   }

   final public double getPreferredMaxX() {
      return this.xmax;
   }

   final public double getPreferredMinY() {
      return this.ymin;
   }

   final public double getPreferredMaxY() {
      return this.ymax;
   }

   final public double getPreferredMinZ() {
      return this.zmin;
   }

   final public double getPreferredMaxZ() {
      return this.zmax;
   }

   final double[] getCenter() {
      return new double[]{centerX, centerY, centerZ};
   }

   final double getMaximum3DSize() {
      double dx = xmax-xmin, dy = ymax-ymin, dz = zmax-zmin;
      switch(camera.getProjectionMode()) {
         case Camera.MODE_PLANAR_XY :
            return Math.max(dx, dy);
         case Camera.MODE_PLANAR_XZ :
            return Math.max(dx, dz);
         case Camera.MODE_PLANAR_YZ :
            return Math.max(dy, dz);
         default :
            return Math.max(Math.max(dx, dy), dz); /* 3D */
      }
   }

   public void zoomToFit() {
      double minX = Double.POSITIVE_INFINITY, maxX = Double.NEGATIVE_INFINITY;
      double minY = Double.POSITIVE_INFINITY, maxY = Double.NEGATIVE_INFINITY;
      double minZ = Double.POSITIVE_INFINITY, maxZ = Double.NEGATIVE_INFINITY;
      double[] firstPoint = new double[3], secondPoint = new double[3];
      Iterator it = getElements().iterator();
      while(it.hasNext()) {
         ((Element) it.next()).getExtrema(firstPoint, secondPoint);
         minX = Math.min(Math.min(minX, firstPoint[0]), secondPoint[0]);
         maxX = Math.max(Math.max(maxX, firstPoint[0]), secondPoint[0]);
         minY = Math.min(Math.min(minY, firstPoint[1]), secondPoint[1]);
         maxY = Math.max(Math.max(maxY, firstPoint[1]), secondPoint[1]);
         minZ = Math.min(Math.min(minZ, firstPoint[2]), secondPoint[2]);
         maxZ = Math.max(Math.max(maxZ, firstPoint[2]), secondPoint[2]);
      }
      double max = Math.max(Math.max(maxX-minX, maxY-minY), maxZ-minZ);
      if(max==0.0) {
         max = 2;
      }
      if(minX>=maxX) {
         minX = maxX-max/2;
         maxX = minX+max;
      }
      if(minY>=maxY) {
         minY = maxY-max/2;
         maxY = minY+max;
      }
      if(minZ>=maxZ) {
         minZ = maxZ-max/2;
         maxZ = minZ+max;
      }
      setPreferredMinMax(minX, maxX, minY, maxY, minZ, maxZ);
   }

   public void setSquareAspect(boolean square) {
      // added by W. Christian
      if(squareAspect!=square) { // only recompute if there is a change
         needsToRecompute = true;
         updatePanel();
      }
      squareAspect = square;
      // computeConstants(); // removed by W. Christian
   }

   public boolean isSquareAspect() {
      return squareAspect;
   }

   public org.opensourcephysics.display3d.core.VisualizationHints getVisualizationHints() {
      return visHints;
   }

   public org.opensourcephysics.display3d.core.Camera getCamera() {
      return camera;
   }

   /**
    * Gets the video capture tool. May be null.
    *
    * @return the video capture tool
    */
   public VideoTool getVideoTool() {
      return vidCap;
   }

   /**
    * Sets the video capture tool. May be set to null.
    *
    * @param videoCap the video capture tool
    */
   public void setVideoTool(VideoTool videoCap) {
      if(vidCap!=null) {
         vidCap.setVisible(false); // hide the current video capture tool
      }
      vidCap = videoCap;
   }

   public void addElement(org.opensourcephysics.display3d.core.Element element) {
      if(!(element instanceof Element)) {
         throw new UnsupportedOperationException("Can't add element to panel (incorrect implementation)");
      }
      if(!elementList.contains(element)) {
         elementList.add(element);
      }
      ((Element) element).setPanel(this);
      dirtyImage = true; // element has been added so image is dirtry
   }

   public void removeElement(org.opensourcephysics.display3d.core.Element element) {
      elementList.remove(element);
      dirtyImage = true; // element has been added so image is dirtry
   }

   public void removeAllElements() {
      elementList.clear();
      dirtyImage = true; // element has been added so image is dirtry
   }

   public synchronized ArrayList getElements() {
      return(ArrayList) elementList.clone();
   }

   /**
    * Shows a message in a yellow text box in the lower right hand corner.
    *
    * @param msg
    */
   public void setMessage(String msg) {
      brMessageBox.setText(msg); // the default message box
   }

   /**
    * Shows a message in a yellow text box.
    * The location must be one of the following:
    * <ul>
    *   <li> DrawingPanel3D.BOTTOM_LEFT;
    *   <li> DrawingPanel3D.BOTTOM_RIGHT;
    *   <li> DrawingPanel3D.TOP_RIGHT;
    *   <li> DrawingPanel3D.TOP_LEFT;
    * </ul>
    * @param msg
    * @param location
    */
   public void setMessage(String msg, int location) {
      switch(location) {
         case BOTTOM_LEFT :
            blMessageBox.setText(msg);
            break;
         default :
         case BOTTOM_RIGHT :
            brMessageBox.setText(msg);
            break;
         case TOP_RIGHT :
            trMessageBox.setText(msg);
            break;
         case TOP_LEFT :
            tlMessageBox.setText(msg);
            break;
      }
   }

   // ---------------------------------
   // Implementation of core.InteractionSource
   // ---------------------------------
   public org.opensourcephysics.display3d.core.interaction.InteractionTarget getInteractionTarget(int target) {
      return myTarget;
   }

   public void addInteractionListener(InteractionListener listener) {
      if((listener==null)||listeners.contains(listener)) {
         return;
      }
      listeners.add(listener);
   }

   public void removeInteractionListener(InteractionListener listener) {
      listeners.remove(listener);
   }

   /**
    * Invokes the interactionPerformed() method of all registered
    * interaction listeners
    * @param event InteractionEvent
    */
   private void invokeActions(InteractionEvent event) {
      Iterator it = listeners.iterator();
      while(it.hasNext()) {
         ((InteractionListener) it.next()).interactionPerformed(event);
      }
   }

   // ----------------------------------------------------
   // All the painting stuff
   // ----------------------------------------------------

   /**
    * Paints everyting assuming an object of the given width and height in pixels.
    * @param g Graphics
    * @param width int
    * @param height int
    */
   private synchronized void paintEverything(Graphics g, int width, int height) {
      // W. Christian recompute scale if something has changed
      if(needsToRecompute||(width!=getWidth())||(height!=getHeight())) {
         computeConstants(width, height);
      }
      ArrayList tempList = getElements();
      tempList.addAll(decorationList);
      g.setColor(getBackground());
      g.fillRect(0, 0, width, height); // fill the component with the background color
      paintDrawableList(g, tempList);
   }

   private void paintDrawableList(Graphics g, ArrayList tempList) {
      Graphics2D g2 = (Graphics2D) g;
      Iterator it = tempList.iterator();
      if(quickRedrawOn||!visHints.isRemoveHiddenLines()) { // Do a quick sketch of the scene
         while(it.hasNext()) {
            ((Element) it.next()).drawQuickly(g2);
         }
         return;
      }
      // Collect objects, sort and draw them one by one. Takes time!!!
      list3D.clear();
      while(it.hasNext()) { // Collect all Objects3D
         Object3D[] objects = ((Element) it.next()).getObjects3D();
         if(objects==null) {
            continue;
         }
         for(int i = 0, n = objects.length; i<n; i++) {
            // providing NaN as distance can be used by Drawables3D to hide a given Object3D
            if(!Double.isNaN(objects[i].getDistance())) {
               list3D.add(objects[i]);
            }
         }
      }
      if(list3D.size()<=0) {
         return;
      }
      Object[] objects = list3D.toArray();
      Arrays.sort(objects, comparator);
      for(int i = 0, n = objects.length; i<n; i++) {
         Object3D obj = (Object3D) objects[i];
         obj.getElement().draw(g2, obj.getIndex());
      }
   }

   // ----------------------------------------------------
   // Printable interface
   // ----------------------------------------------------
   public int print(Graphics g, PageFormat pageFormat, int pageIndex) throws PrinterException {
      if(pageIndex>=1) {
         return Printable.NO_SUCH_PAGE;
      }
      if(g==null) {
         return Printable.NO_SUCH_PAGE;
      }
      Graphics2D g2 = (Graphics2D) g;
      double scalex = pageFormat.getImageableWidth()/(double) getWidth();
      double scaley = pageFormat.getImageableHeight()/(double) getHeight();
      double scale = Math.min(scalex, scaley);
      g2.translate((int) pageFormat.getImageableX(), (int) pageFormat.getImageableY());
      g2.scale(scale, scale);
      paintEverything(g2, getWidth(), getHeight());
      return Printable.PAGE_EXISTS;
   }

   // ----------------------------------------------------
   // Projection, package and private methods
   // ----------------------------------------------------

   /**
    * This will be called by VisualizationHints whenever hints change.
    * @see VisualizationHints
    */
   void hintChanged(int hintThatChanged) {
      switch(hintThatChanged) {
         case VisualizationHints.HINT_DECORATION_TYPE :
            switch(visHints.getDecorationType()) {
               case VisualizationHints.DECORATION_NONE :
                  for(int i = 0, n = boxSides.length; i<n; i++) {
                     boxSides[i].setVisible(false);
                  }
                  xAxis.setVisible(false);
                  yAxis.setVisible(false);
                  zAxis.setVisible(false);
                  xText.setVisible(false);
                  yText.setVisible(false);
                  zText.setVisible(false);
                  break;
               case VisualizationHints.DECORATION_CUBE :
                  for(int i = 0, n = boxSides.length; i<n; i++) {
                     boxSides[i].setVisible(true);
                  }
                  xAxis.setVisible(false);
                  yAxis.setVisible(false);
                  zAxis.setVisible(false);
                  xText.setVisible(false);
                  yText.setVisible(false);
                  zText.setVisible(false);
                  break;
               case VisualizationHints.DECORATION_AXES :
                  for(int i = 0, n = boxSides.length; i<n; i++) {
                     boxSides[i].setVisible(false);
                  }
                  xAxis.setVisible(true);
                  yAxis.setVisible(true);
                  zAxis.setVisible(true);
                  xText.setVisible(true);
                  yText.setVisible(true);
                  zText.setVisible(true);
                  break;
            }
            break;
        case VisualizationHints.HINT_AXES_LABELS :
           String[] labels = visHints.getAxesLabels();
           xText.setText(labels[0]);
           yText.setText(labels[1]);
           zText.setText(labels[2]);
           break;
         case VisualizationHints.HINT_CURSOR_TYPE :
            setCursorMode();
            break;
         case VisualizationHints.HINT_SHOW_COORDINATES :
            break; // Actually no dirtyImage is needed...
      }
      dirtyImage = true; // hint has changed so image is dirtry
   }

   /**
    * This will be called by Camera whenever it changes.
    * @see Camera
    */
   void cameraChanged(int howItChanged) {
      switch(howItChanged) {
         case Camera.CHANGE_MODE :
            double dx = xmax-xmin, dy = ymax-ymin, dz = zmax-zmin;
            maximumSize = getMaximum3DSize();
            resetDecoration(dx, dy, dz);
            // W. Christian constants should be computed when everything is painted
            // computeConstants();
            needsToRecompute = true;
            updatePanel(); // always update if we change display mode
            break;
      }
      reportTheNeedToProject();
      dirtyImage = true; // camera has changed so image is dirtry
   }

   /**
    * Converts a 3D point of the scene into a 2D point of the screen.
    * It also provides a number measuring the relative distance of the point
    * to the camera.
    * distance = 1.0 means at the center of the scene,
    * distance > 1.0 means farther than the center of the scene,
    * distance < 1.0 means closer than the center of the scene,
    * @param coordinate The coordinates of the point of the scene
    * The input coordinates are not modified.
    * @param pixel A place-holder for the coordinates of the point of the screen.
    * It returns a,b and the distance
    * @return The coordinates of the point of the screen and a number
    * which reports about the distance to us
    */
   double[] project(double[] p, double[] pixel) {
      double[] projected = camera.getTransformation().direct((double[]) p.clone());
      double factor = 1.8;
      switch(camera.getProjectionMode()) {
         case Camera.MODE_NO_PERSPECTIVE :
            factor = 1.3;
         case Camera.MODE_PERSPECTIVE :
            factor = 1;
      }
      pixel[0] = acenter+projected[0]*factor*aconstant;
      pixel[1] = bcenter-projected[1]*factor*bconstant;
      pixel[2] = projected[2];
      return pixel;
   }

   /**
    * Converts a world size at a given point into a size in the screen
    * @param p double[] The coordinates of the point at which the 3D
    * size was measured.
    * @param size double[] The size in the X,Y,Z coordinates
    * @param pixelSize double[] A place-holder for the result
    * @return double[] returns the same input pixelSize
    */
   double[] projectSize(double[] p, double[] size, double[] pixelSize) {
      camera.projectSize(p, size, pixelSize);
      double factor = 1.8;
      switch(camera.getProjectionMode()) {
         case Camera.MODE_NO_PERSPECTIVE :
            factor = 1.3;
         case Camera.MODE_PERSPECTIVE :
            factor = 1;
      }
      pixelSize[0] *= factor*aconstant;
      pixelSize[1] *= factor*bconstant;
      return pixelSize;
   }

   /**
    * Computes the display color of a given drawable3D based on its original color and its depth.
    * Transparency of the original color is not affected.
    * @param _aColor the original color
    * @param _depth the depth value of the color
    */
   Color projectColor(Color _aColor, double _depth) {
      if(!visHints.isUseColorDepth()) {
         return _aColor;
      }
      // if      (_depth<0.9) return _aColor.brighter().brighter();
      // else if (_depth>1.1) return _aColor.darker().darker();
      // else return _aColor;
      float[] crc = new float[4]; // Stands for ColorRGBComponent
      try {
         _aColor.getRGBComponents(crc);
         // Do not affect transparency
         for(int i = 0; i<3; i++) {
            crc[i] /= _depth;
            crc[i] = (float) Math.max(Math.min(crc[i], 1.0), 0.0);
         }
         return new Color(crc[0], crc[1], crc[2], crc[3]);
      } catch(Exception _exc) {
         return _aColor;
      }
   }

   /**
    * Converts a point on the screen into a world point
    * It only works properly for planar display modes
    */
   private double[] worldPoint(int a, int b) {
      double factor = 1.8;
      switch(camera.getProjectionMode()) {
         case Camera.MODE_PLANAR_XY :
            return new double[]{centerX+(a-acenter)/(factor*aconstant), centerY+(bcenter-b)/(factor*bconstant), zmax};
         case Camera.MODE_PLANAR_XZ :
            return new double[]{centerX+(a-acenter)/(factor*aconstant), ymax, centerZ+(bcenter-b)/(factor*bconstant)};
         case Camera.MODE_PLANAR_YZ :
            return new double[]{xmax, centerY+(a-acenter)/(factor*aconstant), centerZ+(bcenter-b)/(factor*bconstant)};
         default : /* 3D */
            return new double[]{centerX, centerY, centerZ};
      }
   }

   /**
    * Converts into a world distance a distance on the screen
    * It only works properly for planar display modes
    */
   private double[] worldDistance(int dx, int dy) {
      double factor = 1.8;
      switch(camera.getProjectionMode()) {
         case Camera.MODE_PLANAR_XY :
            return new double[]{dx/(factor*aconstant), -dy/(factor*bconstant), 0.0};
         case Camera.MODE_PLANAR_XZ :
            return new double[]{dx/(factor*aconstant), 0.0, -dy/(factor*bconstant)};
         case Camera.MODE_PLANAR_YZ :
            return new double[]{0.0, dx/(factor*aconstant), -dy/(factor*bconstant)};
         default : /* 3D */
            return new double[]{dx/(1.3*aconstant), dy/(1.3*bconstant), 0.0};
      }
   }

   /**
    * Computes the constants for the given size in pixels.
    * @param width int
    * @param height int
    */
   private void computeConstants(int width, int height) {
      acenter = width/2;
      bcenter = height/2;
      if(squareAspect) {
         width = height = Math.min(width, height);
      }
      aconstant = 0.5*width/maximumSize;
      bconstant = 0.5*height/maximumSize;
      reportTheNeedToProject();
      needsToRecompute = false;
   }

   private void reportTheNeedToProject() {
      Iterator it = getElements().iterator();
      while(it.hasNext()) {
         ((Element) it.next()).setNeedToProject(true);
      }
      it = ((ArrayList) decorationList.clone()).iterator();
      while(it.hasNext()) {
         ((Element) it.next()).setNeedToProject(true);
      }
   }

   private void resetDecoration(double _dx, double _dy, double _dz) {
      boxSides[0].setXYZ(xmin, ymin, zmin);
      boxSides[0].setSizeXYZ(_dx, 0.0, 0.0);
      boxSides[1].setXYZ(xmax, ymin, zmin);
      boxSides[1].setSizeXYZ(0.0, _dy, 0.0);
      boxSides[2].setXYZ(xmin, ymax, zmin);
      boxSides[2].setSizeXYZ(_dx, 0.0, 0.0);
      boxSides[3].setXYZ(xmin, ymin, zmin);
      boxSides[3].setSizeXYZ(0.0, _dy, 0.0);
      boxSides[4].setXYZ(xmin, ymin, zmax);
      boxSides[4].setSizeXYZ(_dx, 0.0, 0.0);
      boxSides[5].setXYZ(xmax, ymin, zmax);
      boxSides[5].setSizeXYZ(0.0, _dy, 0.0);
      boxSides[6].setXYZ(xmin, ymax, zmax);
      boxSides[6].setSizeXYZ(_dx, 0.0, 0.0);
      boxSides[7].setXYZ(xmin, ymin, zmax);
      boxSides[7].setSizeXYZ(0.0, _dy, 0.0);
      boxSides[8].setXYZ(xmin, ymin, zmin);
      boxSides[8].setSizeXYZ(0.0, 0.0, _dz);
      boxSides[9].setXYZ(xmax, ymin, zmin);
      boxSides[9].setSizeXYZ(0.0, 0.0, _dz);
      boxSides[10].setXYZ(xmax, ymax, zmin);
      boxSides[10].setSizeXYZ(0.0, 0.0, _dz);
      boxSides[11].setXYZ(xmin, ymax, zmin);
      boxSides[11].setSizeXYZ(0.0, 0.0, _dz);
      xAxis.setXYZ(xmin, ymin, zmin);
      xAxis.setSizeXYZ(_dx, 0.0, 0.0);
      xText.setXYZ(xmax+_dx*0.02, ymin, zmin);
      yAxis.setXYZ(xmin, ymin, zmin);
      yAxis.setSizeXYZ(0.0, _dy, 0.0);
      yText.setXYZ(xmin, ymax+_dy*0.02, zmin);
      zAxis.setXYZ(xmin, ymin, zmin);
      zAxis.setSizeXYZ(0.0, 0.0, _dz);
      zText.setXYZ(xmin, ymin, zmax+_dz*0.02);
   }

   // ----------------------------------------------------
   // Private methods for the cursor
   // ----------------------------------------------------
   private void setCursorMode() {
      switch(visHints.getCursorType()) {
         case VisualizationHints.CURSOR_NONE :
            trackersVisible = 0;
            break;
         case VisualizationHints.CURSOR_CUBE :
            trackersVisible = 9;
            break;
         default :
         case VisualizationHints.CURSOR_XYZ :
            trackersVisible = 3;
            break;
         case VisualizationHints.CURSOR_CROSSHAIR :
            trackersVisible = 3;
            break;
      }
   }

   private void showTrackers(boolean value) {
      for(int i = 0, n = trackerLines.length; i<n; i++) {
         if(i<trackersVisible) {
            trackerLines[i].setVisible(value);
         } else {
            trackerLines[i].setVisible(false);
         }
      }
   }

   private void positionTrackers() {
      switch(visHints.getCursorType()) {
         case VisualizationHints.CURSOR_NONE :
            return;
         default :
         case VisualizationHints.CURSOR_XYZ :
            trackerLines[0].setXYZ(trackerPoint[0], ymin, zmin);
            trackerLines[0].setSizeXYZ(0, trackerPoint[1]-ymin, 0);
            trackerLines[1].setXYZ(xmin, trackerPoint[1], zmin);
            trackerLines[1].setSizeXYZ(trackerPoint[0]-xmin, 0, 0);
            trackerLines[2].setXYZ(trackerPoint[0], trackerPoint[1], zmin);
            trackerLines[2].setSizeXYZ(0, 0, trackerPoint[2]-zmin);
            break;
         case VisualizationHints.CURSOR_CUBE :
            trackerLines[0].setXYZ(xmin, trackerPoint[1], trackerPoint[2]);
            trackerLines[0].setSizeXYZ(trackerPoint[0]-xmin, 0, 0);
            trackerLines[1].setXYZ(trackerPoint[0], ymin, trackerPoint[2]);
            trackerLines[1].setSizeXYZ(0, trackerPoint[1]-ymin, 0);
            trackerLines[2].setXYZ(trackerPoint[0], trackerPoint[1], zmin);
            trackerLines[2].setSizeXYZ(0, 0, trackerPoint[2]-zmin);
            trackerLines[3].setXYZ(trackerPoint[0], ymin, zmin);
            trackerLines[3].setSizeXYZ(0, trackerPoint[1]-ymin, 0);
            trackerLines[4].setXYZ(xmin, trackerPoint[1], zmin);
            trackerLines[4].setSizeXYZ(trackerPoint[0]-xmin, 0, 0);
            trackerLines[5].setXYZ(trackerPoint[0], ymin, zmin);
            trackerLines[5].setSizeXYZ(0, 0, trackerPoint[2]-zmin);
            trackerLines[6].setXYZ(xmin, ymin, trackerPoint[2]);
            trackerLines[6].setSizeXYZ(trackerPoint[0]-xmin, 0, 0);
            trackerLines[7].setXYZ(xmin, trackerPoint[1], zmin);
            trackerLines[7].setSizeXYZ(0, 0, trackerPoint[2]-zmin);
            trackerLines[8].setXYZ(xmin, ymin, trackerPoint[2]);
            trackerLines[8].setSizeXYZ(0, trackerPoint[1]-ymin, 0);
            break;
         case VisualizationHints.CURSOR_CROSSHAIR :
            trackerLines[0].setXYZ(xmin, trackerPoint[1], trackerPoint[2]);
            trackerLines[0].setSizeXYZ(xmax-xmin, 0.0, 0.0);
            trackerLines[1].setXYZ(trackerPoint[0], ymin, trackerPoint[2]);
            trackerLines[1].setSizeXYZ(0.0, ymax-ymin, 0.0);
            trackerLines[2].setXYZ(trackerPoint[0], trackerPoint[1], zmin);
            trackerLines[2].setSizeXYZ(0.0, 0.0, zmax-zmin);
            break;
      }
   }

   // ----------------------------------------------------
   // Interaction
   // ----------------------------------------------------
   private InteractionTarget getTargetHit(int x, int y) {
      Iterator it = getElements().iterator();
      InteractionTarget target = null;
      while(it.hasNext()) {
         target = ((Element) it.next()).getTargetHit(x, y);
         if(target!=null) {
            return target;
         }
      }
      return null;
   }

   private void setMouseCursor(Cursor cursor) {
      Container c = getTopLevelAncestor();
      setCursor(cursor);
      if(c!=null) {
         c.setCursor(cursor);
      }
   }

   private void displayPosition(double[] _point) {
      visHints.displayPosition(camera.getProjectionMode(), _point);
   }

   // returns true if the tracker was moved
   private boolean mouseDraggedComputations(java.awt.event.MouseEvent e) {
      if(e.isControlDown()) { // Panning
         if(camera.is3dMode()) {
            double fx = camera.getFocusX(), fy = camera.getFocusY(), fz = camera.getFocusZ();
            double dx = (e.getX()-lastX)*maximumSize*0.01, dy = (e.getY()-lastY)*maximumSize*0.01;
            switch(keyPressed) {
               case 88 :      // X is pressed
                  if((camera.cosAlpha>=0)&&(Math.abs(camera.sinAlpha)<camera.cosAlpha)) {
                     camera.setFocusXYZ(fx+dy, fy, fz);
                  } else if((camera.sinAlpha>=0)&&(Math.abs(camera.cosAlpha)<camera.sinAlpha)) {
                     camera.setFocusXYZ(fx+dx, fy, fz);
                  } else if((camera.cosAlpha<0)&&(Math.abs(camera.sinAlpha)<-camera.cosAlpha)) {
                     camera.setFocusXYZ(fx-dy, fy, fz);
                  } else {
                     camera.setFocusXYZ(fx-dx, fy, fz);
                  }
                  break;
               case 89 :      // Y is pressed
                  if((camera.cosAlpha>=0)&&(Math.abs(camera.sinAlpha)<camera.cosAlpha)) {
                     camera.setFocusXYZ(fx, fy-dx, fz);
                  } else if((camera.sinAlpha>=0)&&(Math.abs(camera.cosAlpha)<camera.sinAlpha)) {
                     camera.setFocusXYZ(fx, fy+dy, fz);
                  } else if((camera.cosAlpha<0)&&(Math.abs(camera.sinAlpha)<-camera.cosAlpha)) {
                     camera.setFocusXYZ(fx, fy+dx, fz);
                  } else {
                     camera.setFocusXYZ(fx, fy-dy, fz);
                  }
                  break;
               case 90 :      // Z is pressed
                  if(camera.cosBeta>=0) {
                     camera.setFocusXYZ(fx, fy, fz+dy);
                  } else {
                     camera.setFocusXYZ(fx, fy, fz-dy);
                  }
                  break;
               default :
                  if(camera.cosBeta<0) {
                     dy = -dy;
                  }
                  if((camera.cosAlpha>=0)&&(Math.abs(camera.sinAlpha)<camera.cosAlpha)) {
                     camera.setFocusXYZ(fx, fy-dx, fz+dy);
                  } else if((camera.sinAlpha>=0)&&(Math.abs(camera.cosAlpha)<camera.sinAlpha)) {
                     camera.setFocusXYZ(fx+dx, fy, fz+dy);
                  } else if((camera.cosAlpha<0)&&(Math.abs(camera.sinAlpha)<-camera.cosAlpha)) {
                     camera.setFocusXYZ(fx, fy+dx, fz-dy);
                  } else {
                     camera.setFocusXYZ(fx-dx, fy, fz-dy);
                  }
                  break;
            }
         }
         return false;
      }                       // End of panning
      if(e.isShiftDown()) { // Zooming
         camera.setDistanceToScreen(camera.getDistanceToScreen()-(e.getY()-lastY)*maximumSize*0.01);
         return false;
      }
      if(camera.is3dMode()&&(targetHit==null)&&!e.isAltDown()) { // Rotating (in 3D)
         camera.setAzimuthAndAltitude(camera.getAzimuth()-(e.getX()-lastX)*0.01,
                                      camera.getAltitude()+(e.getY()-lastY)*0.005);
         return false;
      }
      if(trackerPoint==null) {
         return true;
      }
      // In all other cases, you are moving the tracker
      double[] point = worldDistance(e.getX()-lastX, e.getY()-lastY);
      if(!camera.is3dMode()) { // 2D modes
         switch(keyPressed) {
            case 88 :
               trackerPoint[0] += point[0];
               break;          // X is pressed
            case 89 :
               trackerPoint[1] += point[1];
               break;          // Y is pressed
            case 90 :
               trackerPoint[2] += point[2];
               break;          // Z is pressed
            default :
               trackerPoint[0] += point[0];
               trackerPoint[1] += point[1];
               trackerPoint[2] += point[2];
               break;          // No key is pressed
         }
      }                        // End of 2D modes
          else {
         switch(keyPressed) {
            case 88 :          // X is pressed
               if((camera.cosAlpha>=0)&&(Math.abs(camera.sinAlpha)<camera.cosAlpha)) {
                  trackerPoint[0] += point[1];
               } else if((camera.sinAlpha>=0)&&(Math.abs(camera.cosAlpha)<camera.sinAlpha)) {
                  trackerPoint[0] -= point[0];
               } else if((camera.cosAlpha<0)&&(Math.abs(camera.sinAlpha)<-camera.cosAlpha)) {
                  trackerPoint[0] -= point[1];
               } else {
                  trackerPoint[0] += point[0];
               }
               break;
            case 89 :          // Y is pressed
               if((camera.cosAlpha>=0)&&(Math.abs(camera.sinAlpha)<camera.cosAlpha)) {
                  trackerPoint[1] += point[0];
               } else if((camera.sinAlpha>=0)&&(Math.abs(camera.cosAlpha)<camera.sinAlpha)) {
                  trackerPoint[1] += point[1];
               } else if((camera.cosAlpha<0)&&(Math.abs(camera.sinAlpha)<-camera.cosAlpha)) {
                  trackerPoint[1] -= point[0];
               } else {
                  trackerPoint[1] -= point[1];
               }
               break;
            case 90 :          // Z is pressed
               if(camera.cosBeta>=0) {
                  trackerPoint[2] -= point[1];
               } else {
                  trackerPoint[2] -= point[2];
               }
               break;
            default :          // No key is pressed
               if(camera.cosBeta>=0) {
                  trackerPoint[2] -= point[1];
               } else {
                  trackerPoint[2] += point[1];
               }
               if((camera.cosAlpha>=0)&&(Math.abs(camera.sinAlpha)<camera.cosAlpha)) {
                  trackerPoint[1] += point[0];
               } else if((camera.sinAlpha>=0)&&(Math.abs(camera.cosAlpha)<camera.sinAlpha)) {
                  trackerPoint[0] -= point[0];
               } else if((camera.cosAlpha<0)&&(Math.abs(camera.sinAlpha)<-camera.cosAlpha)) {
                  trackerPoint[1] -= point[0];
               } else {
                  trackerPoint[0] += point[0];
               }
               break;
         }
      }                        // End of 3D modes
      return true;
   }

   private void resetInteraction() {
      targetHit = null;
      showTrackers(false);
      displayPosition(null);
      // blMessageBox.setText(null);
      // repaint();  removed by W. Christian
      dirtyImage = true;
      updatePanel();
   }

   /**
    * The inner class that will handle all mouse related events.
    */
   private class IADMouseController extends MouseController {

      public void mousePressed(MouseEvent _evt) {
         requestFocus();
         if (_evt.isPopupTrigger() || _evt.getModifiers() == InputEvent.BUTTON3_MASK) return;
//         quickRedrawOn = visHints.isAllowQuickRedraw() || keyPressed==83;  // 's' is pressed
         /*
         if(visHints.isAllowQuickRedraw()&&((_evt.getModifiers()&InputEvent.BUTTON1_MASK)!=0)) {
            quickRedrawOn = true;
         } else {
            quickRedrawOn = false;
         }
*/
         lastX = _evt.getX();
         lastY = _evt.getY();
         targetHit = getTargetHit(lastX, lastY);
         if(targetHit!=null) {
            Element el = targetHit.getElement();
            trackerPoint = el.getHotSpot(targetHit);
            el.invokeActions(new InteractionEvent(el, InteractionEvent.MOUSE_PRESSED, targetHit.getActionCommand(),
                                                  targetHit, _evt));
            trackerPoint = el.getHotSpot(targetHit);     // because the listener may change the position of the element
         } else if(myTarget.isEnabled()) {               // No interactive has been hit
            if((!camera.is3dMode())||_evt.isAltDown()) { // In 2D by default, in 3D only if you hold ALT down
               // You are trying to track a given point
               trackerPoint = worldPoint(_evt.getX(), _evt.getY());
               invokeActions(new InteractionEvent(DrawingPanel3D.this, InteractionEvent.MOUSE_PRESSED,
                                                  myTarget.getActionCommand(), trackerPoint, _evt));
            } else {
               invokeActions(new InteractionEvent(DrawingPanel3D.this, InteractionEvent.MOUSE_PRESSED,
                                                  myTarget.getActionCommand(), null, _evt));
               resetInteraction();
               return;
            }
         } else {
            resetInteraction();
            return;
         }
         displayPosition(trackerPoint);
         positionTrackers();
         showTrackers(true);
         // repaint();  removed by W. Christian
         dirtyImage = true;
         updatePanel();
      }

      public void mouseReleased(MouseEvent _evt) {
         if (_evt.isPopupTrigger() || _evt.getModifiers() == InputEvent.BUTTON3_MASK) return;
         if(targetHit!=null) {
            Element el = targetHit.getElement();
            el.invokeActions(new InteractionEvent(el, InteractionEvent.MOUSE_RELEASED, targetHit.getActionCommand(),
                                                  targetHit, _evt));
         } else if(myTarget.isEnabled()) {
            if((!camera.is3dMode())||_evt.isAltDown()) { // In 2D by default, in 3D only if you hold ALT down
               invokeActions(new InteractionEvent(DrawingPanel3D.this, InteractionEvent.MOUSE_RELEASED,
                                                  myTarget.getActionCommand(), trackerPoint, _evt));
            } else {
               invokeActions(new InteractionEvent(DrawingPanel3D.this, InteractionEvent.MOUSE_RELEASED,
                                                  myTarget.getActionCommand(), null, _evt));
            }
         }
         quickRedrawOn = false;
         resetInteraction();
         // setMouseCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
      }

      public void mouseDragged(MouseEvent _evt) {
         if (_evt.isPopupTrigger() || _evt.getModifiers() == InputEvent.BUTTON3_MASK) return;
         quickRedrawOn = visHints.isAllowQuickRedraw() && keyPressed!=83;
         boolean trackerMoved = mouseDraggedComputations(_evt);
         lastX = _evt.getX();
         lastY = _evt.getY();
         if(!trackerMoved) { // Report any listener that the projection has changed. Data is NULL!
            invokeActions(new InteractionEvent(DrawingPanel3D.this, InteractionEvent.MOUSE_DRAGGED,
                                               myTarget.getActionCommand(), null, _evt));
            resetInteraction();
            return;
         }
         if(targetHit!=null) {
            Element el = targetHit.getElement();
            el.updateHotSpot(targetHit, trackerPoint);
            el.invokeActions(new InteractionEvent(el, InteractionEvent.MOUSE_DRAGGED, targetHit.getActionCommand(),
                                                  targetHit, _evt));
            trackerPoint = el.getHotSpot(targetHit); // The listener may change the position of the element
            displayPosition(trackerPoint);
            positionTrackers();
            showTrackers(true);                      // should trackers appear only in 3D mode?
         } else if(myTarget.isEnabled()) {
            invokeActions(new InteractionEvent(DrawingPanel3D.this, InteractionEvent.MOUSE_DRAGGED,
                                               myTarget.getActionCommand(), trackerPoint, _evt));
            displayPosition(trackerPoint);
            positionTrackers();
            showTrackers(true);                      // should trackers appear only in 3D mode?
         }
         // repaint();  removed by W. Christian
         dirtyImage = true;
         updatePanel();
      }

      public void mouseEntered(MouseEvent _evt) {
         setMouseCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
         if(myTarget.isEnabled()) {
            invokeActions(new InteractionEvent(DrawingPanel3D.this, InteractionEvent.MOUSE_ENTERED,
                                               myTarget.getActionCommand(), null, _evt));
         }
         targetHit = targetEntered = null;
      }

      public void mouseExited(MouseEvent _evt) {
         setMouseCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
         if(myTarget.isEnabled()) {
            invokeActions(new InteractionEvent(DrawingPanel3D.this, InteractionEvent.MOUSE_EXITED,
                                               myTarget.getActionCommand(), null, _evt));
         }
         targetHit = targetEntered = null;
      }

      public void mouseClicked(MouseEvent _evt) {}

      public void mouseMoved(MouseEvent _evt) {
         InteractionTarget target = getTargetHit(_evt.getX(), _evt.getY());
         if(target!=null) {
            if(targetEntered==null) {
               target.getElement().invokeActions(new InteractionEvent(target.getElement(),
                   InteractionEvent.MOUSE_ENTERED, target.getActionCommand(), target, _evt));
            }
            setMouseCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
         } else { // No target under the cursor
            if(targetEntered!=null) {
               targetEntered.getElement().invokeActions(new InteractionEvent(targetEntered.getElement(),
                   InteractionEvent.MOUSE_EXITED, targetEntered.getActionCommand(), targetEntered, _evt));
            } else if(myTarget.isEnabled()) {
               invokeActions(new InteractionEvent(DrawingPanel3D.this, InteractionEvent.MOUSE_MOVED,
                                                  myTarget.getActionCommand(), null, _evt));
            }
            setMouseCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
         }
         targetEntered = target;
      }
   }

   private class GlassPanel extends javax.swing.JPanel {

      public void render(Graphics g) {
         Component[] c = glassPanelLayout.getComponents();
         for(int i = 0, n = c.length; i<n; i++) {
            if(c[i]==null) {
               continue;
            }
            g.translate(c[i].getX(), c[i].getY());
            c[i].print(g);
            g.translate(-c[i].getX(), -c[i].getY());
         }
      }
   }

   // ----------------------------------------------------
   // XML loader
   // ----------------------------------------------------

   /**
    * Returns an XML.ObjectLoader to save and load object data.
    * @return the XML.ObjectLoader
    */
   public static XML.ObjectLoader getLoader() {
      return new DrawingPanel3DLoader();
   }

   static private class DrawingPanel3DLoader extends org.opensourcephysics.display3d.core.DrawingPanel3D.Loader {

      public Object createObject(XMLControl control) {
         return new DrawingPanel3D();
      }

      public Object loadObject(XMLControl control, Object obj) {
         super.loadObject(control, obj);
         DrawingPanel3D panel = (DrawingPanel3D) obj;
         // Load the visualization hints
         VisualizationHints hints = (VisualizationHints) control.getObject("visualization hints");
         hints.setPanel(panel);
         panel.visHints = hints;
         panel.hintChanged(VisualizationHints.HINT_DECORATION_TYPE);
         // Load the camera
         Camera cam = (Camera) control.getObject("camera");
         cam.setPanel(panel);
         panel.camera = cam;
         panel.cameraChanged(Camera.CHANGE_ANY);
         // Order a render()
         panel.needsToRecompute = true;
         panel.dirtyImage = true; // new data so image is dirtry
         panel.updatePanel();
         return obj;
      }
   } // End of static class DrawingPanel3DLoader
}
/*
 * Open Source Physics software is free software; you can redistribute
 * it and/or modify it under the terms of the GNU General Public License (GPL) as
 * published by the Free Software Foundation; either version 2 of the License,
 * or(at your option) any later version.
 *
 * Code that uses any portion of the code in the org.opensourcephysics package
 * or any subpackage (subdirectory) of this package must must also be be released
 * under the GNU GPL license.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston MA 02111-1307 USA
 * or view the license online at http://www.gnu.org/copyleft/gpl.html
 *
 * Copyright (c) 2007  The Open Source Physics project
 *                     http://www.opensourcephysics.org
 */
